package optimizer;

import simulator.*;

import java.util.*;
import java.util.Map.Entry;

/**
 * This is the main optimizing strategy. 
 * It simulates a large number of strategies in the hope it randomly encounters a good strategy
 * See report for more details on it's strategy
 * 
 * implements runnable so that we can run it in multiple threads
 */
public class Optimizer implements Runnable{
	
	Simulator simulator;
	
	GameState initial;
	GameState minimumTarget;
	GameState maximumTarget;
	
	int maxNumberOfTries, upperLimit;
	
	public Strategy fastestStrategy = null;
	
	LinkedList<GameChoice> neglectedChoices = new LinkedList<GameChoice>();
	LinkedList<GameAsset> savingFor = new LinkedList<GameAsset>();
	
	/**
	 *  The initial GameState and the minimum and maximum target get initialised
	 *  
	 * @param simulator
	 * @param target
	 * @param maxNumberOfTries
	 * @param upperLimit
	 */
	public Optimizer(Simulator simulator, Target target, int maxNumberOfTries, int upperLimit){
		this.simulator = simulator;
		this.initial = target.getInitial();
		this.minimumTarget = target.getMinimumTarget();
		this.maximumTarget = target.getMaximumTarget();
		
		this.maxNumberOfTries = maxNumberOfTries;
		this.upperLimit = upperLimit;
	}
	
	/**
	 * Attempts to find a faster strategy in the maxNumberOfTries given
	 * 
	 * @param maxNumberOfTries - the number of attempts to make to get a faster strategy
	 * @param upperLimit - the fastest time achieved so far
	 * @return the fastest strategy found, or null if none is found
	 */
	public Strategy findFastestStrategy(int maxNumberOfTries, int upperLimit) {
		int fastestTimeSoFar = upperLimit;
		int currentTries = 0;
		Strategy fastestStrategy = null;

		// While we haven't reached the maxNumberOfTries
		while (currentTries < maxNumberOfTries) {
			// Attempt to find a valid strategy
			Strategy newStrategy = findValidStrategy(fastestTimeSoFar);

			// if we did find a valid strategy
			if(newStrategy != null) {
				// and it is faster than the previous fastest
				if(newStrategy.getTime() < fastestTimeSoFar) {
					// update the fastest variables
					fastestTimeSoFar = newStrategy.getTime();
					fastestStrategy = newStrategy;
					System.out.println("Reached target at " + fastestTimeSoFar + "seconds");
				}
			}
			// and increment the number of tries we've had
			currentTries++;
		}

		return fastestStrategy;
	}
	
	/**
	 * tries to come up with a valid strategy that is shorter than the fastest time
	 * 
	 * @param fastestTime - a strategy taking longer than the fastest time doesn't need to be persued
	 * @return strategy - which is null if it couldn't be completed in less than the fastest time
	 */
	private Strategy findValidStrategy(int fastestTime) {
		
		GameState current = new GameState(this.initial);
		Strategy strategy = new Strategy();
		ArrayList<GameChoice> choices = new ArrayList<GameChoice> ();
		GameChoice chosen;
	
		// GameChoices are made until the current GameTtate reaches the minimum target
		while(!current.reachedTarget(minimumTarget)) {
			
			if(current.getTime() >= fastestTime || current.passedMaximum(maximumTarget)) {
				return null;
			}
			
			if(!current.reachedTargetTotal(minimumTarget))
			{
				// workers get reassigned according to where they are more helpful
				current = assignWorkers(current);
				
				// all possible choices are received from the simulator
				choices = simulator.getAllAvailableChoices(current);
			
				chosen = chooseStrategy(current, choices);
				
				/*if (chosen.getGameAsset() != null){
					System.out.println("(" + current.getTime()+ ") " + chosen.getGameAsset());
				}*/
				
				
				// the current game state get's updated with the chosen move and the move is added to the strategy
				current = simulator.choose(current, chosen);
				strategy.addMove(chosen, current.getTime());
			}
			
			// the current GameState gets updated and prepared for the next game second
			simulator.tickState(current);
		}
		
		strategy.setStrategyCompleted(current.getTime());
		
		return strategy;
	}
	
	/**
	 * chooses a strategy from the suggested choices taking into account neglected choices that can't be chosen and
	 * what would contribute to reaching the target
	 * if mulpiple options satisfy the criteria it chooses at random
	 * 
	 * @param current
	 * @param choices
	 * @return GameChoice that got chosen from all choices
	 */
	private GameChoice chooseStrategy(GameState current,
			ArrayList<GameChoice> choices) {

		/*System.out.println("\tChoices:");
		for (GameChoice choice : choices) {
			System.out.println("\t\t" + choice.getGameAsset());
		}*/

		// appropriate choices are choices that are relevant for reaching the
		// goal
		LinkedList<GameChoice> appropriateChoices = OptimizerUtilities.getAppropriateChoices(current, choices, neglectedChoices, minimumTarget, this.maximumTarget);
		// assets that need saving are relevant assets that it can't afford yet
		LinkedList<GameAsset> assetsThatNeedSaving = OptimizerUtilities.assetsThatNeedSaving(current, this.maximumTarget);

		// assets that it could achieve based on dependancies but can't afford
		// are worth saving up for
		LinkedList<GameAsset> assetsThatNeedSavingAndNeedAchieving = new LinkedList<GameAsset>();

		for (GameAsset gameAsset : assetsThatNeedSaving) {
			if ((current.getTotalNumberOfAssets(gameAsset) < 1)
					&& (minimumTarget.getNumberOfAsset(gameAsset) == 0)
					|| !OptimizerUtilities.isAchievedInMinimumTarget(current,minimumTarget, gameAsset)) {

				// only add an asset to the array being saved for if we have all
				// of it's
				// direct dependencies (either currently or being built)

				GameObject obj = GameDataStore
						.getGameObjectFromGameAsset(gameAsset);

				boolean haveAllDependencies = true;

				for (Entry<GameAsset, Integer> dependency : obj
						.getDependencies().entrySet()) {
					if (current.getNumberOfAsset(dependency.getKey()) < dependency
							.getValue()) {
						haveAllDependencies = false;
					}
				}

				if (haveAllDependencies) {
					assetsThatNeedSavingAndNeedAchieving.add(gameAsset);
				}
			}
		}

		// allows to save up for a supply depot if it is not needed to increase
		// supply but as a dependency
		if ((current.getTotalNumberOfAssets(GameAsset.SUPPLY_DEPOT) > 0)
				&& (assetsThatNeedSavingAndNeedAchieving.contains(GameAsset.SUPPLY_DEPOT))) {
			
			assetsThatNeedSavingAndNeedAchieving.remove(GameAsset.SUPPLY_DEPOT);
		}
		/*System.out.println("\tWaiting for:");
		for (GameAsset choice : assetsThatNeedSavingAndNeedAchieving) {
			System.out.println("\t\t" + choice);
		}*/

		GameChoice chosen = null;

		for (GameChoice gameChoice : appropriateChoices) {
			if (savingFor.contains(gameChoice.getGameAsset())) {
				chosen = gameChoice;
			}
		}

		Random random = new Random();
		if (chosen == null) {
			// a random appropriate choice is picked
			chosen = appropriateChoices.get(random.nextInt(appropriateChoices.size()));
		}

		boolean neglected = false;
		if (chosen.getGameAsset() == null) {
			// if there is nothing worth saving up for, it is only allowed to
			// wait if there are no other appropriate choices
			if ((assetsThatNeedSavingAndNeedAchieving.size() == 0)&& (appropriateChoices.size() > 1)) {
				chosen = appropriateChoices.get(random.nextInt(appropriateChoices.size()));
			} else {
				// choices that were not picked in order to save up for
				// something get neglected until the asset that
				// is being saved up for is reached
				neglected = true;
				for (GameAsset gameAsset : assetsThatNeedSavingAndNeedAchieving) {
					if (!savingFor.contains(gameAsset)) {
						savingFor.add(gameAsset);
					}
				}
				neglectedChoices = OptimizerUtilities.neglectChoices(appropriateChoices, neglectedChoices);
			}
		}
		if (savingFor.contains(chosen.getGameAsset())) {
			savingFor.remove(chosen.getGameAsset());
		}

		// if neglected choices are needed for the target they can only be
		// neglected temproarily
		if (!neglected) {
			neglectedChoices = OptimizerUtilities.unNeglectChoices(
					appropriateChoices, neglectedChoices, minimumTarget);
		}
		
		/*System.out.println("\tNeglected choices:");
		for (GameChoice choice : neglectedChoices) {
			System.out.println("\t\t" + choice.getGameAsset());
		}*/
		
		return chosen;
	}
	
	
	/**
	 * assigns the workers to either gathering minerals or gathering gas, depending on what is needed the most
	 * 
	 * @param current
	 * @return the gamestate with the new worker assignment taken into account
	 */
	private GameState assignWorkers(GameState current) {

		int totalWorkers = current.getNumberOfAsset(GameAsset.WORKER) - current.getNumWorkersInState(WorkerState.BUILDING);
		
		// 3 workers can work on one mineral patch at the same time
		int mineralPatchJobs = current.getNumberOfAsset(GameAsset.MINERAL_PATCH) * 3;
		
		// 3 workers can work on one refinery at the same time
		int refineryJobs = current.getNumberOfAsset(GameAsset.REFINERY) * 3;
		
		GameAsset[] assets = OptimizerUtilities.getTotalGameAssetArrayFromGameState(current);
		
		int requiredMinerals = maximumTarget.getNumberOfAsset(GameAsset.MINERAL) - (current.getNumberOfAsset(GameAsset.MINERAL) + OptimizerUtilities.calculateTotalMineralsRequired(assets));
		int requiredGas = maximumTarget.getNumberOfAsset(GameAsset.VESPENE_GAS) - (current.getNumberOfAsset(GameAsset.VESPENE_GAS) + OptimizerUtilities.calculateTotalVespeneRequired(assets));

		if(requiredGas == 0 || refineryJobs == 0)
		{
			current.setWorkersTask(totalWorkers, 0);
		}
		else if(requiredMinerals == 0 || mineralPatchJobs == 0)
		{
			current.setWorkersTask(0, totalWorkers);
		}
		else
		{	
			// workers get distributed according to what is needed more urgently
			double mineralNeed = requiredMinerals / (double)(requiredMinerals + requiredGas);
			
			int numMineralWorkers = (int)Math.round(mineralNeed * totalWorkers);
			int numGasWorkers = totalWorkers - numMineralWorkers;
			
			current.setWorkersTask(numMineralWorkers, numGasWorkers);
		}
		
		return current;
	}

	/**
	 * Runnable method for threads
	 */
	@Override
	public void run()
	{
		fastestStrategy = this.findFastestStrategy(this.maxNumberOfTries, this.upperLimit);
	}
	
}

